/*
 * Copyright (C) 2009 aileron.cc
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package cc.aileron.orm.factory;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.List;

import cc.aileron.accessor.PojoAccessor;
import cc.aileron.accessor.PojoAccessorFactory;
import cc.aileron.commons.instance.InstanceFactory;
import cc.aileron.commons.utils.SkipList;
import cc.aileron.orm.G2Id;
import cc.aileron.orm.G2DaoFindCondition.Paging;
import cc.aileron.orm.factory.serial.G2DaoSerial;
import cc.aileron.orm.logger.SqlLogger;
import cc.aileron.orm.logger.SqlLoggerParameter;
import cc.aileron.orm.sql.G2DaoDto;
import cc.aileron.orm.sql.G2DaoSqlMap;

/**
 * @author Aileron
 * @param <Bean>
 */
public class G2DaoExecutorImpl<Bean> implements G2DaoExecutor<Bean>
{
    @Override
    public int count(final Connection connection,
            final Object condition) throws SQLException
    {
        final String name = "findBy" + condition.getClass()
            .getSimpleName();
        final PreparedStatement preparedStatement = createPreparedStatement(
                name, connection, condition);
        final ResultSet resultSet = preparedStatement.executeQuery();
        if (resultSet.last())
        {
            return resultSet.getRow();
        }
        return 0;
    }

    @Override
    public void delete(final Connection connection,
            final Object bean) throws SQLException
    {
        final String name = "delete";
        createPreparedStatement(name, connection, bean).execute();
    }

    @Override
    public boolean exist(final Connection connection,
            final Object condition) throws SQLException
    {
        final String name = "findBy" + condition.getClass()
            .getSimpleName();
        final PreparedStatement preparedStatement = createPreparedStatement(
                name, connection, condition);

        final ResultSet resultSet = preparedStatement.executeQuery();
        return resultSet.next();
    }

    @Override
    public List<Bean> findList(final Connection connection,
            final Object condition) throws SQLException
    {
        final String name = "findBy" + condition.getClass()
            .getSimpleName();
        final PreparedStatement preparedStatement = createPreparedStatement(
                name, connection, condition);
        final ResultSet resultSet = preparedStatement.executeQuery();
        final List<String> columnNames = getColumnNames(resultSet);
        final List<Bean> result = new SkipList<Bean>();
        while (resultSet.next())
        {
            final PojoAccessor<Bean> accessor = accessorFactory.create(instanceFactory.create(targetClass));
            bind(accessor, columnNames, resultSet);
            result.add(accessor.toTarget());
        }
        return result;
    }

    @Override
    public List<Bean> findList(final Connection connection,
            final Object condition,
            final Paging paging) throws SQLException
    {
        final String name = "findBy" + condition.getClass()
            .getSimpleName();
        final PreparedStatement preparedStatement = createPreparedStatement(
                name, connection, condition);
        final ResultSet resultSet = preparedStatement.executeQuery();
        final List<String> columnNames = getColumnNames(resultSet);
        final List<Bean> result = new SkipList<Bean>();

        final int offset = paging.offset();
        final int limit = paging.limit();

        int i = 0;
        while (resultSet.next())
        {
            i += 1;

            if (offset > i)
            {
                continue;
            }

            if (limit < i)
            {
                resultSet.close();
                break;
            }

            final PojoAccessor<Bean> accessor = accessorFactory.create(instanceFactory.create(targetClass));
            bind(accessor, columnNames, resultSet);
            result.add(accessor.toTarget());
        }
        return result;
    }

    @Override
    public Bean findOne(final Connection connection,
            final Object condition) throws SQLException
    {
        final String name = "findBy" + condition.getClass()
            .getSimpleName();
        final PreparedStatement preparedStatement = createPreparedStatement(
                name, connection, condition);

        final ResultSet resultSet = preparedStatement.executeQuery();
        final List<String> columnNames = getColumnNames(resultSet);
        final PojoAccessor<Bean> accessor = accessorFactory.create(instanceFactory.create(targetClass));
        if (!resultSet.next())
        {
            return null;
        }
        bind(accessor, columnNames, resultSet);
        return accessor.toTarget();
    }

    @Override
    public void initialize(final Connection connection) throws SQLException
    {
        final String name = "initialize";
        createPreparedStatement(name, connection, new Object()).execute();
    }

    @Override
    public void insert(final Connection connection,
            final Bean bean) throws SQLException
    {
        final String name = "insert";
        final PreparedStatement ps = createPreparedStatement(name, connection,
                bean);
        ps.execute();
        final G2Id id = bean.getClass()
            .getAnnotation(G2Id.class);
        if (id != null)
        {
            serial.getSerialNumber(ps, id.value(), bean);
        }
    }

    @Override
    public void update(final Connection connection,
            final Bean bean) throws SQLException
    {
        final String name = "update";
        createPreparedStatement(name, connection, bean).execute();
    }

    /**
     * @param accessor
     * @param columnNames
     * @param resultSet
     * @throws SQLException
     */
    private void bind(final PojoAccessor<?> accessor,
            final List<String> columnNames,
            final ResultSet resultSet) throws SQLException
    {
        final int size = columnNames.size();
        for (int i = 0; i < size; i++)
        {
            final String key = columnNames.get(i);
            final Object val = resultSet.getObject(i + 1);
            accessor.set(key, val);
        }
    }

    /**
     * PreparedStatement の生成
     */
    private PreparedStatement createPreparedStatement(final String name,
            final Connection connection,
            final Object bean) throws SQLException
    {
        final G2DaoDto dto = new G2DaoDto(bean);
        final PojoAccessor<G2DaoDto> accessor = accessorFactory.create(dto);
        final StringWriter stringWriter = new StringWriter();
        final PrintWriter printWriter = new PrintWriter(stringWriter);
        sqlMap.get(targetClass, name)
            .print(accessor, printWriter);
        printWriter.flush();
        printWriter.close();

        final String sql = stringWriter.toString();

        if (isDebug)
        {
            final SqlLoggerParameter parameter = new SqlLoggerParameter(
                    targetClass.getName() + "#" + name, sql, dto.parameters);
            sqlLogger.output(parameter);
        }

        final PreparedStatement preparedStatement = connection.prepareStatement(
                sql, ResultSet.TYPE_SCROLL_INSENSITIVE,
                ResultSet.CONCUR_UPDATABLE);
        for (int max = dto.parameters.size(), i = 0; i < max; i++)
        {
            preparedStatement.setObject(i + 1, dto.parameters.get(i));
        }
        return preparedStatement;
    }

    /**
     * @param resultSet
     * @return ラベルの集合
     * @throws SQLException
     */
    private List<String> getColumnNames(final ResultSet resultSet) throws SQLException
    {
        final SkipList<String> result = new SkipList<String>();
        final ResultSetMetaData metaData = resultSet.getMetaData();
        final int count = metaData.getColumnCount();
        for (int i = 0; i < count; i++)
        {
            result.add(metaData.getColumnLabel(i + 1));
        }
        return result;
    }

    /**
     * @param targetClass
     * @param serial
     * @param instanceFactory
     * @param accessorFactory
     * @param sqlMap
     * @param sqlLogger
     */
    public G2DaoExecutorImpl(
        final Class<Bean> targetClass,
        final G2DaoSerial serial,
        final InstanceFactory instanceFactory,
        final PojoAccessorFactory accessorFactory,
        final G2DaoSqlMap sqlMap,
        final SqlLogger sqlLogger)
    {
        this.targetClass = targetClass;
        this.serial = serial;
        this.instanceFactory = instanceFactory;
        this.accessorFactory = accessorFactory;
        this.sqlMap = sqlMap;
        this.sqlLogger = sqlLogger;
        this.isDebug = sqlLogger.isEnabled();
    }

    private final boolean isDebug;

    private final PojoAccessorFactory accessorFactory;

    private final InstanceFactory instanceFactory;

    private final G2DaoSerial serial;

    private final SqlLogger sqlLogger;

    private final G2DaoSqlMap sqlMap;

    private final Class<Bean> targetClass;

}
